/***************************************************************************
 SimpleMail - Copyright (C) 2000 Hynek Schlawack and Sebastian Bauer

 This program is free software; you can redistribute it and/or modify
 it under the terms of the GNU General Public License as published by
 the Free Software Foundation; either version 2 of the License, or
 (at your option) any later version.

 This program is distributed in the hope that it will be useful,
 but WITHOUT ANY WARRANTY; without even the implied warranty of
 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 GNU General Public License for more details.

 You should have received a copy of the GNU General Public License
 along with this program; if not, write to the Free Software
 Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
***************************************************************************/

/**
 * @file ssl.c
 */

#ifndef NO_SSL

#ifdef __MORPHOS__
#	include <openssl/ssl.h>
#	include <openssl/x509v3.h>
#else
#	if defined(_AMIGA ) || defined(__AMIGAOS4__) /* ugly */
#		include <proto/amissl.h> /* not portable */
#	else
#		include <openssl/ssl.h>
#		include <openssl/x509v3.h>
#	endif
#endif

#include "configuration.h"
#include "debug.h"
#include "ssl.h"
#include "curl_hostcheck.h"
#include "smintl.h"
#include "subthreads.h"
#include "support.h"



/**
 * Returns the subject name (CN) of a certificate
 *
 * @param cert The server certificate
 * @param name Destination memory
 * @param size Size of destination memory
 * @return 0 on success
 */
static int ssl_get_subject_name(X509 * cert, char * name, size_t size)
{
	int rc = X509_NAME_get_text_by_NID(X509_get_subject_name(cert), NID_commonName, name, size);
	if(rc < 0) {
		strcpy(name, "Not found");
	}
	return rc;
}


/**
 * Returns the issuer name (CN) of a certificate
 *
 * @param cert The server certificate
 * @param name Destination memory
 * @param size Size of destination memory
 * @return 0 on success
 */
static int ssl_get_issuer_name(X509 * cert, char * name, size_t size)
{
	int rc = X509_NAME_get_text_by_NID(X509_get_issuer_name(cert), NID_commonName, name, size);
	if(rc < 0) {
		strcpy(name, "Not found");
	}
	return rc;
}


/**
 * Returns the fingerprint of a certificate and build an ASCII representation
 *
 * @param cert  The server certificate
 * @param sha1  Destination memory
 * £param ascii ASCII representation
 * @return 0 on success
 */
static int ssl_get_fingerprint(X509 * cert, unsigned char * sha1,  char * ascii)
{
	unsigned int size;
	int rc = X509_digest(cert, EVP_sha1(), sha1, &size);
	
	if(rc) {
		int i;
		
		/* Build fingerprint string */
		for (i = 0; i < size; i++) {
			sm_snprintf(&ascii[i*3], 4, "%02X  ", sha1[i]);
		}
		
		ascii[size*3] = '\0';
	}
	
	return rc;
}


/**
 * Verifies the passed host against the verificate. SAN first, then CN.
 *
 * @param conn Defines the already secured connection
 * @param cert The server certificate
 * @return SM_SSL_OK on success
 */
int ssl_verify_host(struct connection * conn, X509 * cert)
{
	int sanmatch = 0;
	STACK_OF(GENERAL_NAME) * altnames;
	
	/* Get list of alternative names from SAN extension */
	altnames = X509_get_ext_d2i(cert, NID_subject_alt_name, NULL, NULL);
	if(altnames) {
		int i, numalts;
		
		numalts = sk_GENERAL_NAME_num(altnames);
		
		for(i = 0; i < numalts; i++) {
			const GENERAL_NAME * cn = sk_GENERAL_NAME_value(altnames, i);
			const char * altptr;
			size_t altlen;
			
			/* Perform domain name checks only */
			if(cn->type != GEN_DNS) continue;
			
			altptr = (char *)ASN1_STRING_data(cn->d.ia5);
			altlen = (size_t)ASN1_STRING_length(cn->d.ia5);
			
			/* Blame embedded zeros */
			if(strlen(altptr) == altlen) {
				char debug[256];
		
				/* Abuse CURL hostcheck for our needs */
				if(Curl_cert_hostcheck(altptr, conn->hostname) == CURL_HOST_MATCH) {
					sm_snprintf(debug, sizeof(debug), "Host match, %s = %s.\n", altptr, conn->hostname);
					sanmatch = 1;
				} else {
					sm_snprintf(debug, sizeof(debug), "Host did not match, %s != %s.\n", altptr, conn->hostname);	
				}
				
				SM_DEBUGF(5, (debug));

				if(sanmatch) {
					break;
				}
			} else {
				SM_DEBUGF(5, ("Malformed certificate\n"));
			}
		}
		
		sk_GENERAL_NAME_pop_free(altnames, GENERAL_NAME_free);
	}
	
	if(sanmatch) {
		/* SAN matched. Cool. */
		SM_DEBUGF(5, ("SAN matched.\n"));
	} else {
		/* SAN did not match, try CN now. */
		int cn_idx;
		X509_NAME_ENTRY * cn_entry;
		ASN1_STRING * cn_asn1;
		char * cn_str;
		
		/* Get CN index in subject */
		cn_idx = X509_NAME_get_index_by_NID(X509_get_subject_name(cert), NID_commonName, -1);
		if(cn_idx < 0) {
			SM_DEBUGF(5, ("Could not get CN index\n"));
			return SM_SSL_HOST_FAILED;
		}
		
		/* Get CN field in subject  */
		cn_entry = X509_NAME_get_entry(X509_get_subject_name(cert), cn_idx);
		if(cn_entry == NULL) {
			SM_DEBUGF(5, ("Could not get CN field\n"));
			return SM_SSL_HOST_FAILED;
		}
		
		/* Get ASN1 of CN and convert to C string */
		cn_asn1 = X509_NAME_ENTRY_get_data(cn_entry);
		if(cn_asn1 == NULL) {
			SM_DEBUGF(5, ("Could not get CN ASN1\n"));
			return SM_SSL_HOST_FAILED;
		}
		cn_str = (char *)ASN1_STRING_data(cn_asn1);
		
		/* Blame embedded zeros */
		if(strlen(cn_str) != (size_t)ASN1_STRING_length(cn_asn1)) {
			SM_DEBUGF(5, ("Malformed certificate\n"));
			return SM_SSL_HOST_FAILED;
		}
		
		/* Finally compare CN with hostname */
		if(Curl_cert_hostcheck(cn_str, conn->hostname) == CURL_HOST_NOMATCH) {
			char debug[256];
			
			sm_snprintf(debug, sizeof(debug), "CN did not match, %s != %s.\n", cn_str, conn->hostname);
			SM_DEBUGF(5, (debug));
			
			return SM_SSL_HOST_FAILED;
		} else {
			SM_DEBUGF(5, ("CN matched.\n"));
		}
	}

	return SM_SSL_OK;
}


/**
 * A user can trust a certificate
 *
 * @param conn Defines the already secured connection
 * @return SM_SSL_OK on success
 */
int ssl_can_be_trusted(struct connection * conn)
{
	char          issuer_name[256];
	char          subject_name[256];
	char          summary[1024];
	unsigned char sha1[EVP_MAX_MD_SIZE];
	char          sha1_ascii[EVP_MAX_MD_SIZE*3+1];
	int           sha1_rc;
	int           peer_rc;
	int           dialog_rc;
	
	/* Having the peer certificate is a must */
	X509 *server_cert = SSL_get_peer_certificate(conn->ssl);
	if(!server_cert) {
		SM_DEBUGF(5, ("Could not get peer cerficate.\n"));
		return SM_SSL_PEER_FAILED;
	}
	
	/* Get result of peer verification */
	peer_rc = SSL_get_verify_result(conn->ssl);
	
	/* Get subject name */
	ssl_get_subject_name(server_cert, subject_name, sizeof(subject_name));
	
	/* Get issuer name */
	ssl_get_issuer_name(server_cert, issuer_name, sizeof(issuer_name));
	
	/* Get fingerprint */
	sha1_rc = ssl_get_fingerprint(server_cert, sha1, sha1_ascii);
	if(!sha1_rc) {
		SM_DEBUGF(5, ("Could not create fingerprint"));
		return SM_SSL_CERT_FAILED;
	}
	
	/* And free the cert before checking the results */
	X509_free(server_cert);
		
	/* Display requester */
	sm_snprintf(summary, sizeof(summary), "Issued to: %s (CN)\nIssues by: %s (CN)", subject_name, issuer_name);

	dialog_rc = thread_call_function_sync(thread_get_main(), sm_request, 6,
			NULL, _("Failed to verify server certificate:\n%s\n\n%s\nSHA1: %s"), _("Always trust|Trust|Abort"),
			peer_rc != X509_V_OK ? X509_verify_cert_error_string(peer_rc) : _("Host verification failed"), 
			summary, 
			sha1_ascii);

	/* If user pressed "Always trust", save the fingerprint */
	if(dialog_rc == 1) {
		SM_DEBUGF(5,("Saving certificate fingerprint\n"));
		if(user.trustedcerts_filename) {
			FILE *f = fopen(user.trustedcerts_filename, "at");
			if(f) {
				fprintf(f, "%s\t%s\t%s\n", subject_name, issuer_name, sha1_ascii);
				fclose(f);
			} else {
				SM_DEBUGF(5,("Could not save certificate fingerprint (fopen)\n"));
				return SM_SSL_CERT_FAILED;
			}
		} else {
			SM_DEBUGF(5,("Could not save certificate fingerprint (config)\n"));
			return SM_SSL_CERT_FAILED;
		}
	}
	
	SM_DEBUGF(5,("Connection is assumed to be secure\n"));
	return SM_SSL_OK;
}



/**
 * Verifies the passed cert
 *
 * @param conn Defines the already secured connection
 * @param perr Peer failed in verify_callback?
 * @return SM_SSL_OK on success
 */
int ssl_verify_cert(struct connection * conn, int perr)
{
	char          issuer_name[256];
	char          subject_name[256];
	unsigned char sha1[EVP_MAX_MD_SIZE];
	char          sha1_ascii[EVP_MAX_MD_SIZE*3+1];
	int           subject_rc;
	int           issuer_rc;
	int           sha1_rc;
	int           peer_rc;
	int           host_rc;
	int           sha1_found = 0;

	/* Having the peer certificate is a must */
	X509 *server_cert = SSL_get_peer_certificate(conn->ssl);
	if(!server_cert) {
		SM_DEBUGF(5, ("Could not get peer cerficate.\n"));
		return SM_SSL_PEER_FAILED;
	}
	
	/* Get all necessary information about the cerficate and do not bail out
	   if something is missing. We're checking that later because here many
	   situations are possible. */
	
	/* Get result of peer verification */
	peer_rc = SSL_get_verify_result(conn->ssl);
	
	/* Get subject name */
	subject_rc = ssl_get_subject_name(server_cert, subject_name, sizeof(subject_name));
	
	/* Get issuer name */
	issuer_rc = ssl_get_issuer_name(server_cert, issuer_name, sizeof(issuer_name));
	
	/* Get fingerprint */
	sha1_rc = ssl_get_fingerprint(server_cert, sha1, sha1_ascii);
	
	/* Verify host */
	host_rc = ssl_verify_host(conn, server_cert);
	
	/* And free the cert before checking the results */
	X509_free(server_cert);
	
	/* Now we collected all data. First we search the certificate in our 
	   trusted cerficates database. These are even allowed without host validation */
	if(!sha1_rc) {
		SM_DEBUGF(5, ("Could not create fingerprint"));
	} else {
		/* And search for the fingerprint in our "database" */   
		SM_DEBUGF(5, ("Checking trusted certificates\n"));
		if(user.trustedcerts_filename) {
			FILE *f = fopen(user.trustedcerts_filename, "rt");
			if(f) {
				char tl[4096];
				char l[4096];
				
				sprintf(tl, "%s\t%s\t%s", subject_name, issuer_name, sha1_ascii);
				
				while(fgets(l, sizeof(l), f)) {
					if(l[0] == '#') continue;
					
					l[strlen(l)-1] = '\0';
					
					if(!strcmp(l, tl)) {
						SM_DEBUGF(5, ("Found fingerprint\n"));
						sha1_found = 1;
						break;
					}
				}
				SM_DEBUGF(5, ("Could not find fingerprint in trusted certificates.\n"));
			} else {
				SM_DEBUGF(5, ("Could not read file with trusted certificates.\n"));
			}
		}
		
		/* Fingerprint was found. Cool. */
		if(sha1_found) {
			return SM_SSL_OK;
		}
	}
	
	/* If certificate and host verifies, the connection is secure */
	if(perr || peer_rc != X509_V_OK) {
		SM_DEBUGF(5, ("Peer verification failed.\n"));
		return SM_SSL_PEER_FAILED;
	} else if(host_rc != SM_SSL_OK) {
		SM_DEBUGF(5, ("Host verification failed.\n"));
		return SM_SSL_HOST_FAILED;
	}
	
	/* Peer and host verify. Cool. */
	return SM_SSL_OK;
}


/**
 * Initialize ssl and return the primary context.
 *
 * @return
 */
SSL_CTX *ssl_init(void)
{
	const SSL_METHOD * m;
	SSL_CTX *ctx;

	SSLeay_add_ssl_algorithms();
	
	ERR_load_crypto_strings();
	ERR_load_SSL_strings();
	ERR_load_BIO_strings();
	SSL_load_error_strings();
	
	m = SSLv23_client_method();
	
	SM_DEBUGF(5, ("Creating SSL context\n"));
	
	if ((ctx = SSL_CTX_new(m)))
	{
		const char *cypher_list = user.config.ssl_cypher_list;

		if (!cypher_list)
			cypher_list = "HIGH:!aNULL:!eNULL:!EXPORT:!DES:!3DES:!MD5:!PSK@STRENGTH";
	
		if(!user.config.ssl_allow_v2) SSL_CTX_set_options(ctx, SSL_OP_NO_SSLv2);
		if(!user.config.ssl_allow_v3) SSL_CTX_set_options(ctx, SSL_OP_NO_SSLv3);
		
		if (SSL_CTX_set_cipher_list(ctx, cypher_list))
		{
			char * capath = mycombinepath(user.directory, "ca-bundle.crt");
			char debug[1024];
			
			sm_snprintf(debug, sizeof(debug), "Reading CA certs from %s\n", capath);
			
			SM_DEBUGF(5,(debug));
			
			if (SSL_CTX_load_verify_locations(ctx, capath, NULL)) 
			{
				return ctx;
			} else {
				SM_DEBUGF(5,("SSL_CTX_load_verify_locations() failed.\n"));
			}
		} else {
			SM_DEBUGF(5,("SSL_CTX_set_cipher_list() failed.\n"));
		}
	}
	return NULL;
}

#endif
